package com.gitee.wsl.text.replacer

import com.gitee.wsl.ext.base.CharExt
import com.gitee.wsl.text.replacer.NumericEntityDecoder.Option

/**
 * Helper subclass to StringTranslator to allow for translations that
 * will replace up to one character at a time.
 */
abstract class CodePointReplacer : AbstractStrReplacer() {
    override fun replace(str: CharSequence, pos: Int, out: Appendable): Int{
        val codePoint: Int = str[pos].code
        val consumed = translate(codePoint, out)
        return if (consumed) 1 else 0
    }

    /**
     * Translates the specified code point into another.
     *
     * @param codePoint Int character input to translate
     * @param stringBuilder StringBuilder to optionally push the translated output to
     * @return boolean as to whether translation occurred or not
     */
    abstract fun translate(codePoint: Int, stringBuilder: Appendable): Boolean


}

class NumericEntityEncoder(
    private val range: IntRange,
    private val between: Boolean
) : CodePointReplacer() {

    /**
     * Constructs a `NumericEntityEncoder` for all characters.
     */
    constructor() : this(0..Int.MAX_VALUE, true)

    override fun translate(codePoint: Int, stringBuilder: Appendable): Boolean {
        if (between != range.contains(codePoint)) {
            return false
        }

        stringBuilder.append("&#")
        stringBuilder.append(codePoint.toString(10))
        stringBuilder.append(';')
        return true
    }

    companion object {
        /**
         * Constructs a `NumericEntityEncoder` above the specified value (exclusive).
         *
         * @param codePoint above which to encode
         * @return The newly created `NumericEntityEncoder` instance
         */
        fun above(codePoint: Int): NumericEntityEncoder {
            return outsideOf(0, codePoint)
        }

        /**
         * Constructs a `NumericEntityEncoder` below the specified value (exclusive).
         *
         * @param codePoint below which to encode
         * @return The newly created `NumericEntityEncoder` instance
         */
        fun below(codePoint: Int): NumericEntityEncoder {
            return outsideOf(codePoint, Int.MAX_VALUE)
        }

        /**
         * Constructs a `NumericEntityEncoder` between the specified values (inclusive).
         *
         * @param codePointLow above which to encode
         * @param codePointHigh below which to encode
         * @return The newly created `NumericEntityEncoder` instance
         */
        fun between(codePointLow: Int, codePointHigh: Int): NumericEntityEncoder {
            return NumericEntityEncoder(codePointLow..codePointHigh, true)
        }

        /**
         * Constructs a `NumericEntityEncoder` outside of the specified values (exclusive).
         *
         * @param codePointLow below which to encode
         * @param codePointHigh above which to encode
         * @return The newly created `NumericEntityEncoder` instance
         */
        fun outsideOf(codePointLow: Int, codePointHigh: Int): NumericEntityEncoder {
            return NumericEntityEncoder(codePointLow..codePointHigh, false)
        }
    }
}

fun StrReplacer.Companion.numericEntityEncoder(range: IntRange, between: Boolean)
             = NumericEntityEncoder(range, between)


class NumericEntityDecoder(vararg options: Option) : AbstractStrReplacer() {
    /** Enumerates NumericEntityDecoder options for unescaping.  */
    enum class Option {
        /**
         * Requires a semicolon.
         */
        SemiColonRequired,

        /**
         * Does not require a semicolon.
         */
        SemiColonOptional,

        /**
         * Throws an exception if a semicolon is missing.
         */
        ErrorIfNoSemiColon
    }

    /** EnumSet of OPTIONS, given from the constructor, read-only.  */
    private val options: Set<Option> = if (options.isEmpty()) DEFAULT_OPTIONS else setOf(*options)

    /**
     * Tests whether the passed in option is currently set.
     *
     * @param option to check state of
     * @return whether the option is set
     */
    private fun isSet(option: Option): Boolean {
        return options.contains(option)
    }

    override fun replace(str: CharSequence, pos: Int, out: Appendable): Int{
        val seqEnd = str.length
        // Uses -2 to ensure there is something after the &#
        if (str[pos] == '&' && pos < seqEnd - 2 && str[pos + 1] == '#') {
            var start = pos + 2
            var isHex = false
            val firstChar = str[start]

            if (firstChar == 'x' || firstChar == 'X') {
                start++
                isHex = true

                // Check there's more than just an x after the &#
                if (start == seqEnd) {
                    return 0
                }
            }
            var end = start
            // Note that this supports character codes without a ; on the end
            while (
                end < seqEnd && (
                        str[end] in '0'..'9'
                                || str[end] in 'a'..'f'
                                || str[end] in 'A'..'F'
                        )
            ) {
                end++
            }

            val semiNext = end != seqEnd && str[end] == ';'

            if (!semiNext) {
                if (isSet(Option.SemiColonRequired)) {
                    return 0
                }
                if (isSet(Option.ErrorIfNoSemiColon)) {
                    throw IllegalArgumentException("Semi-colon required at end of numeric entity")
                }
            }
            val entityValue: Int = try {
                if (isHex) {
                    str.subSequence(start, end).toString().toInt(16)
                } else {
                    str.subSequence(start, end).toString().toInt(10)
                }
            } catch (e: NumberFormatException) {
                return 0
            }

            if (entityValue > 0xFFFF) {
                CharExt.toChars(entityValue).forEach { out.append(it) }
            } else {
                out.append(Char(entityValue))
            }
            return 2 + end - start + (if (isHex) 1 else 0) + if (semiNext) 1 else 0
        }
        return 0
    }

    companion object {
        /** Default options.  */
        private val DEFAULT_OPTIONS: Set<Option> = setOf(
            Option.SemiColonRequired
        )

        val INSTANCE = NumericEntityDecoder()
    }
}

fun StrReplacer.Companion.numericEntityDecoder(vararg options: Option) = NumericEntityDecoder(*options)

/**
 * Helper subclass to StringTranslator to remove unpaired surrogates.
 */
 class UnicodeUnpairedSurrogateRemover : CodePointReplacer() {
    override fun translate(codePoint: Int, stringBuilder: Appendable): Boolean {
        // If true, it is a surrogate. Write nothing and say we've translated. Otherwise return false, and don't translate it.
        return codePoint >= Char.MIN_SURROGATE.code && codePoint <= Char.MAX_SURROGATE.code
    }
}